/* 
 * A simple iterative HTTP/1.0 Web server,
 that uses the GET method to serve static and dynamic content.
 * Test static:
 telnet localhost 12345
 GET / HTTP/1.0

 * Test dynamic:
 telnet localhost 12345
 GET /cgi-bin/adder?1&2 HTTP/1.0

 * Test error:
 telnet localhost 12345
 GET /error HTTP/1.0
**/

#include "error.h"
#include "rio.h"
#include "sock.h"

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

#if defined(_WIN32)
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <arpa/inet.h>
#include <netdb.h>
#endif

extern char **environ;

static void
writen(int fd, char *buf, size_t count)
{
    if (rio_writen(fd, buf, count) < 0)
        unix_error("rio_writen fail");
}

static void
readlineb(RIOT *rp, char *buf, size_t count)
{
    if (rio_readlineb(rp, buf, count) < 0)
        unix_error("rio_readlineb fail");
}

static void
clienterror(int fd, const char *cause, const char *errnum, const char *shortmsg, const char *longmsg)
{
    char buf[10240], body[10240];
    // Buffer may overflow
    // HTTP response body
    snprintf(body, sizeof(body),
            "<html><title>Tiny Error</title>"
            "<body bgcolor=""ffffff"">\r\n"
            "%s: %s\r\n"
            "<p>%s: %s\r\n"
            "<hr><em>The Tiny Web server</em>\r\n",
            errnum, shortmsg, longmsg, cause);

    // response
    snprintf(buf, sizeof(buf), "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
    writen(fd, buf, strlen(buf));
    snprintf(buf, sizeof(buf), "Content-type: text/html\r\n");
    writen(fd, buf, strlen(buf));
    snprintf(buf, sizeof(buf), "Content-length: %d\r\n\r\n", (int)strlen(body));
    writen(fd, buf, strlen(buf));

    writen(fd, body, strlen(body));
    
}

static void
read_requesthdrs(RIOT *rp)
{
    char buf[10240];
    readlineb(rp, buf, sizeof(buf));
    while (strcmp(buf, "\r\n")) {
        readlineb(rp, buf, sizeof(buf));
        printf("%s\n", buf);
    }
}

static int
parse_uri(const char *uri, char *filename, char *cgiargs)
{
    if (!strstr(uri, "cgi-bin")) {
        // Static content
        strcpy(cgiargs, "");
        strcpy(filename, ".");
        strcat(filename, uri);
        if (uri[strlen(uri)-1] == '/')
            strcat(filename, "home.html");
        return 1;

    } else {
        // Dynamic content
        char *ptr = strchr(uri, '?');
        if (ptr) {
            strcpy(cgiargs, ptr+1);
            *ptr = '\0';
        } else {
            strcpy(cgiargs, "");
        }
        strcpy(filename, ".");
        strcat(filename, uri);
        return 0;
    }
}

static void
get_filetype(const char *filename, char *filetype)
{
    if (strstr(filename, ".html"))
        strcpy(filetype, "text/html");
    else if (strstr(filename, ".gif"))
        strcpy(filetype, "image/gif");
    else if (strstr(filename, ".png"))
        strcpy(filetype, "image/png");
    else if (strstr(filename, ".jpg"))
        strcpy(filetype, "image/jpeg");
    else
        strcpy(filetype, "text/plain");
}

static void
serve_static(int fd, const char *filename, int filesize)
{
    char filetype[1024], buf[10240];
    // HTTP response headers
    get_filetype(filename, filetype);
    snprintf(buf, sizeof(buf), 
            "HTTP/1.0 200 OK\r\n"
            "Server: Tiny Web Server\r\n"
            "Connection: close\r\n"
            "Content-length: %d\r\n"
            "Content-type: %s\r\n\r\n", filesize, filetype);
    writen(fd, buf, strlen(buf));
    printf("Response headers:\n%s", buf);

    // response body
    int filefd = open(filename, O_RDONLY, 0);
    if (filefd < 0)
        unix_error("open file fail");
    char *filep = mmap(0, filesize, PROT_READ, MAP_PRIVATE, filefd, 0);
    if (!filep)
        unix_error("mmap fail");
    if (close(filefd) != 0)
        unix_error("close file fail");
        
    // So convenient use memory map to read file content
    writen(fd, filep, filesize);
    if (munmap(filep, filesize) != 0)
        unix_error("munmap fail");
}

static void
serve_dynamic(int fd, const char *filename, const char *cgiargs)
{
    char buf[10240];
    snprintf(buf, sizeof(buf), "HTTP/1.0 200 OK\r\n");
    writen(fd, buf, strlen(buf));
    snprintf(buf, sizeof(buf), "Server: Tiny Web Server\r\n");
    writen(fd, buf, strlen(buf));
    
    char *emptylist[] = {NULL};
    if (fork() == 0) {
        setenv("QUERY_STRING", cgiargs, 1);
        dup2(fd, STDOUT_FILENO); // Redirect stdout to client
        execve(filename, emptylist, environ);
    }
    
    if (wait(NULL) < 0)
        unix_error("wait fail"); // Wait for and reap child
}

static void
doit(int fd)
{
    RIOT rio; 
    char buf[10240], method[1024], uri[1024], version[1024];
    char filename[1024], cgiargs[1024];

    rio_readinitb(&rio, fd);
    readlineb(&rio, buf, sizeof(buf));
    printf("Request headers:\n%s", buf);

    // Buffer may overflow
    sscanf(buf, "%s %s %s", method, uri, version);
    if (strcasecmp(method, "GET")) {
        clienterror(fd, method, "501", "Not implemented", "Tiny does not implemente this method");
        return;
    }

    read_requesthdrs(&rio);

    int is_static = parse_uri(uri, filename, cgiargs);
    struct stat sbuf;
    if (stat(filename, &sbuf) < 0) {
        clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file");
        return;
    }

    if (is_static) {
        if (!S_ISREG(sbuf.st_mode) || !(S_IRUSR && sbuf.st_mode)) {
            clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't read the file");
            return;
        }
        serve_static(fd, filename, sbuf.st_size);
    } else {
        if (!S_ISREG(sbuf.st_mode) || !(S_IXUSR && sbuf.st_mode)) {
            clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program");
            return;
        }
        serve_dynamic(fd, filename, cgiargs);
    }
}

int
main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <port>\n", argv[0]);
        return 1;
    }

    const char *bind_port = argv[1];
    int listenfd = sock_open_listenfd(bind_port);
    if (listenfd < 0) {
        fprintf(stderr, "Open listenfd fail\n");
        return 1;
    }

    int connfd;
    struct sockaddr_storage clientaddr;
    socklen_t clientlen;

    int code;
    char hostname[100];
    char port[10];
    int flag = NI_NUMERICHOST | NI_NUMERICSERV;

    for (;;) {
        clientlen = sizeof(clientaddr);
        connfd = accept(listenfd, (struct sockaddr*)&clientaddr, &clientlen);
        if (connfd < 0)
            unix_error("accept fail");

        if ((code = getnameinfo((struct sockaddr*)&clientaddr, clientlen, hostname, sizeof(hostname), port, sizeof(port), flag)) != 0)
            gai_error(code, "getnameinfo fail");
        printf("Accepted connection from %s:%s\n", hostname, port);

        doit(connfd);

        if (close(connfd) != 0)
            unix_error("close fail");
    }
}

